package SharedCloud
{
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.events.IEventDispatcher;
	import flash.utils.Dictionary;
	import flash.utils.describeType;
	import flash.utils.getDefinitionByName;
	import flash.utils.getQualifiedClassName;
	
	import mx.collections.ArrayCollection;
	import mx.collections.IList;
	import mx.events.CollectionEvent;
	import mx.events.CollectionEventKind;
	import mx.events.PropertyChangeEvent;
	import mx.rpc.events.FaultEvent;
	import mx.rpc.events.ResultEvent;
	import mx.rpc.remoting.RemoteObject;
	import mx.utils.ObjectUtil;
	
	[Event(name="InitialUpdateComplete", type="flash.events.Event")]
	
	public class Cloud extends EventDispatcher
	{
		protected var service:RemoteObject;
		protected var fetching:Boolean = false;
		protected var isFirstUpdate:Boolean = true;
		
		protected var lastUpdate:int = -1;
		
		protected var clientID:int;
		protected var nextLocalID:int = 1;
		
		protected var cache:Cache = new Cache();
		
		public function connect(_service:RemoteObject):void
		{
			if(service)
			{
				service.disconnect();
				service.removeEventListener(ResultEvent.RESULT, resultHandler);
				service.removeEventListener(FaultEvent.FAULT, faultHandler);
			}
			service = _service;
			if(service)
			{
				service.addEventListener(ResultEvent.RESULT, resultHandler);
				service.addEventListener(FaultEvent.FAULT, faultHandler);

				service.getSession();
				service.fetch(-1);
			}
		}
		
		/**
		 * Forces an item in the cloud to update in all
		 * other remote clouds.
		 */
		public function update(obj:SyncedObject):void
		{
			if(service && !fetching)
			{
				//if this is a new object, give it an ID
				if(obj.localID == -1)
				{
					obj.clientID = clientID;
					obj.localID = nextLocalID++;
				}
				var header:SyncHeader = drain(obj);
				service.update(header);
			}
			if(!cache.contains(obj))
			{
				cache.add(obj);
				watch(obj);
			}
		}
		
		/**
		 * Gets all objects of a certain type from a cloud.
		 */
		public function find(type:Class):ArrayCollection
		{
			return cache.findAllOfType(type);
		}
		
		protected function faultHandler(evt:FaultEvent):void
		{
			//I don't really do failure.
		}
		protected function resultHandler(evt:ResultEvent):void
		{
			if(evt.result is Update)
			{
				fetching = true;
				var upd:Update = Update(evt.result);
				lastUpdate = upd.revision;
				
				//first make sure that all objects in the update are in the local cache
				for each(var header:SyncHeader in upd.headers)
				{
					var cached:SyncedObject = cache.find(header.vo['clientID'],header.vo['localID']);
					if(cached)
						lazyCopy(header.vo, cached);
					else
					{
						var type:Class = getDefinitionByName(header['remoteType']) as Class;
						var newObj:SyncedObject = new type();
						lazyCopy(header.vo, newObj);
						update(newObj);
					}
					
				}
				//now that every object from the update in the cache, wire em up
				for each(header in upd.headers)
				{
					cached = cache.find(header.vo['clientID'],header.vo['localID']);
					mergeIncoming(header.incomingRefs, cached);
				}
				fetching = false;
				if(isFirstUpdate)
				{
					dispatchEvent(new Event("InitialUpdateComplete"));
					isFirstUpdate = false;
				}
			}
			else if(evt.result is Session)
			{
				clientID = Session(evt.result).clientID;
			}
			else if(evt.result is int)	//this must the revision from a commit
			{
				lastUpdate = Number(evt.result);
			}
		}
		
		protected var watchedLists:Dictionary = new Dictionary();
		protected function watch(obj:SyncedObject):void
		{
			if(obj is IEventDispatcher)
			{
				IEventDispatcher(obj).addEventListener(PropertyChangeEvent.PROPERTY_CHANGE, propChangeHandler);
				for each(var prop:XML in describeType(obj)..accessor)
				{
					var ref:Ref = Ref.create(obj, String(prop.@name));
					attachToProperty(ref, obj[prop.@name]);
				}
			}
		}	
		protected function propChangeHandler(evt:PropertyChangeEvent):void
		{
			var ref:Ref = Ref.create(SyncedObject(evt.source), String(evt.property));
			if(evt.oldValue is ArrayCollection)
			{
				var list:ArrayCollection = ArrayCollection(evt.oldValue);
				delete watchedLists[list];
				list.removeEventListener(CollectionEvent.COLLECTION_CHANGE, listChangeHandler);
				for each(var item:Object in list)
					if(item is SyncedObject)
						SyncedObject(item).incomingRefs.remove(ref);
			}
			if(evt.oldValue is SyncedObject)
			{
				var oldSO:SyncedObject = SyncedObject(evt.oldValue);
				oldSO.incomingRefs.remove(ref);
				update(oldSO);
			}
			attachToProperty(ref, evt.newValue);
			
			update(SyncedObject(evt.source));
		}
		protected function attachToProperty(parent:Ref, value:Object):void
		{
			if(value == null) return;
			if(value is ArrayCollection)
			{
				var newList:ArrayCollection = ArrayCollection(value);
				watchedLists[newList] = parent;
				newList.addEventListener(CollectionEvent.COLLECTION_CHANGE, listChangeHandler);
				for each(var li:Object in newList)
					if(li is SyncedObject)
					{
						parent.index = newList.getItemIndex(li);
						SyncedObject(li).incomingRefs.add(parent);
					}						
			}
			if(value is SyncedObject)
			{
				var newSO:SyncedObject = SyncedObject(value);
				newSO.incomingRefs.add(parent);
				update(newSO);
			}
		}
		protected function listChangeHandler(evt:CollectionEvent):void
		{
			if(evt.kind != CollectionEventKind.ADD &&
				evt.kind != CollectionEventKind.REMOVE &&
				evt.kind != CollectionEventKind.MOVE)
				return;
			var ref:Ref = ObjectUtil.copy(watchedLists[evt.target]) as Ref;
			for each(var item:Object in evt.items)
			{
				if(item is SyncedObject)
				{
					if(evt.kind == CollectionEventKind.ADD ||
						evt.kind == CollectionEventKind.MOVE)
					{
						//set the itemIndex for this ref
						var coll:IList = evt.target as IList;
						var actualIndex:int = coll.getItemIndex(item);
						var prevRef:Ref = getRefAt(coll, actualIndex-1, ref);
						var nextRef:Ref = getRefAt(coll, actualIndex+1, ref);
											
						/**
						 * The ref's itemIndex is not the actual index in the list.
						 * Instead, its relative to the itemIndexes of the preceding
						 * and following objects in the list.
						 * This "relative item index" means that we only have to update
						 * 1 index when things are added, removed, or moved.
						 * In this scheme, it is common to have negative and decimal itemIndexes
						 */
						if(!prevRef && !nextRef)
							ref.index = actualIndex;	//unlikely case
						else if(prevRef && !nextRef)
							ref.index = Math.ceil(prevRef.index + 1);
						else if(!prevRef && nextRef)
							ref.index = Math.floor(nextRef.index - 1);
						else	//prev && next
							ref.index = (prevRef.index+nextRef.index)/2;
					}
					
					var so:SyncedObject = item as SyncedObject;
					if(evt.kind == CollectionEventKind.ADD)
						so.incomingRefs.add(ref);
					if(evt.kind == CollectionEventKind.MOVE)
						so.incomingRefs.find(ref).index = ref.index;
					if(evt.kind == CollectionEventKind.REMOVE)
						so.incomingRefs.remove(ref);
					update(so);
				}
				else
				{
					var listHost:SyncedObject = cache.findRef(ref);
					update(listHost as SyncedObject);
				}
			}
		}
		
		protected static function lazyCopy(source:Object, dest:Object):void
		{
			for(var prop:String in source)
			{
				var sourceVal:Object = source[prop];
				var destVal:Object = dest[prop];
				if(sourceVal != destVal)
					dest[prop] = sourceVal;
			}
		}
		protected function mergeIncoming(newRefs:ArrayCollection, target:SyncedObject):void
		{
			//remove all incoming links to the target
			while(target.incomingRefs.length > 0)
			{
				var ref:Ref = target.incomingRefs.getRefAt(0);
				var parent:SyncedObject = cache.findRef(ref);
				if(!parent) continue;
				var list:ArrayCollection = parent[ref.property] as ArrayCollection;
				if(list)
				{
					var index:int = list.getItemIndex(target);
					list.removeItemAt(index);
				}
				else
					parent[ref.property] = null;
			}
			
			//now add each newRef
			for(var i:int=0; i<newRefs.length; i++)
			{
				ref = newRefs.getItemAt(i) as Ref;
				parent = cache.findRef(ref);
				if(!parent) continue;
				list = parent[ref.property] as ArrayCollection;
				if(list)
				{
					for(var tryIndex:int=0; tryIndex<=list.length; tryIndex++)
					{
						var prev:Ref = getRefAt(list,tryIndex-1,ref);
						var next:Ref = getRefAt(list,tryIndex,ref);
						if(!prev || prev.index<ref.index)
							if(!next || next.index>=ref.index)
							{
								list.addItemAt(target, tryIndex);
								//the listChangedHandler messed up our itemIndex, so reset it
								target.incomingRefs.find(ref).index = ref.index;
								break;
							}
					}
				}
				else
					parent[ref.property] = target;
			}
		}
		
		private function getRefAt(list:IList, index:int, refToList:Ref):Ref
		{
			if(index < 0 || index >= list.length)
				return null;
			var so:SyncedObject = list.getItemAt(index) as SyncedObject;
			if(!so)
				return null;			
			return so.incomingRefs.find(refToList);
		}
		
		/**
		 * drain takes a SyncedObject and transforms it into a VO
		 */
		protected function drain(obj:SyncedObject):SyncHeader
		{
			var header:SyncHeader = new SyncHeader();
			var vo:Object = new Object();
			for each(var prop:XML in describeType(obj)..accessor)
			{
				var propVal:Object = obj[prop.@name];
				if(propVal == null) continue;
				if(propVal is IList) continue;
				if(propVal is SyncedObject) continue;
				vo[prop.@name] = propVal;
			}
			vo.clientID = obj.clientID;
			vo.localID = obj.localID;
			header.remoteType = getQualifiedClassName(obj);
			header.incomingRefs = obj.incomingRefs.toArray();
			header.vo = vo;
			return header;
		}
	}
}